package controlService;

import models.*;

import java.util.concurrent.*;
import javax.microedition.io.*;
import com.google.gson.Gson;

import models.Command.CommandValue;

import java.io.*;


class RemoteClient implements Runnable
{
	private StreamConnection _connection;
	private BlockingQueue<Command> _outgoing;
	private BlockingQueue<CommandSource> _commandQueue;
	private boolean _isRunning;
	
	public RemoteClient(BlockingQueue<CommandSource> commandQueue, StreamConnection connection)
	{
		_connection = connection;
		_commandQueue = commandQueue;
		_outgoing = new LinkedBlockingQueue<Command>();
		_isRunning = false;
	}
	
	@Override
	public void run()
	{		
		// Notice: we are spinning off another thread on ourselves!  This is sort of like Forking in Unix.  
		// This will allow us to read and write to the Android device at the same time.
		if(!_isRunning)
		{
			_isRunning = true;
			Thread writerThread = new Thread(this);
			writerThread.start();
			
			// Let the Android client know we're up and running
			_outgoing.add(new Command(CommandValue.Handshake));
			
			reader();			
	
			// Reader thread responsible for closing connection
			try { 
				// Wait for the writer to finish up and bail out
				writerThread.join();		
				_connection.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		else
		{
			writer();
		}			
	}
	
	public void enqueueOutgoing(Command cmd)
	{
		_outgoing.add(cmd);
	}
	
	private Command readCommand(byte[] bytes)
	{	
		try	{				
			return Command.Mapper.readValue(bytes, Command.class);				
		} catch (Exception e) {
			System.err.println("Failed to process incomming Command:\n");
			e.printStackTrace();
		}
		return null;
	}
	
	private byte[] writeCommand(Command cmd) throws Exception
	{	
		try {		
			return Command.Mapper.writeValueAsBytes(cmd);
		} catch (Exception e) {
			System.err.println("Failed to serialize Tcard into byte array:\n");
			e.printStackTrace();
			throw e;
		}
	}	
	
	// If the RemoteClient needs to specifically respond to any of the commands before
	// they get passed on to the ControlService, do it here:
	private void handleCommand(Command cmd)
	{				
		switch(cmd.getCommand())
		{
		case CloseConnection:
			_isRunning = false;
			break;
		case Handshake:
			System.out.println("Completed handshake with Android client.");
			break;
		}
		
		_commandQueue.add(new CommandSource(cmd, this));	
	}
	
	private void reader()
	{
		DataInputStream dataInputStream = null;
		
		// set up Bluetooth socket and listen for updates
		try {
			dataInputStream = new DataInputStream(_connection.openInputStream());
			System.out.println("Android client listener initialized.  Waiting for commands...!");
			
			while(_isRunning)
			{	
				// Need to know how big our command payload is
				int size = dataInputStream.readInt();
				byte[] buffer = new byte[size];				
				dataInputStream.readFully(buffer);
				
				Command cmd = readCommand(buffer);
				
				handleCommand(cmd);				
			}
		} catch(EOFException eof) {
			System.err.println("Android device disconnected\n");
			handleCommand(new Command(CommandValue.CloseConnection));
		} catch(Exception e) { 
			System.err.println("Critical failure while reading from Android device:\n");
			e.printStackTrace();
		}			
		finally
		{
			try {
				// Outside loop, means we got a "close connection" command from the client
				if(dataInputStream != null)
					dataInputStream.close();	
			} catch (IOException e) { /* do nothing */ }	
		}
		System.out.println("Android client listener closed.");
	}
	
	private void writer()
	{			   
		DataOutputStream dataOutputStream = null;
		
		try {
			dataOutputStream = new DataOutputStream(_connection.openOutputStream());
			
			while(_isRunning)
			{
				//Transaction t = session.beginTransaction();
				Command cmd = _outgoing.take();		
				byte[] buffer = writeCommand(cmd);
				
				dataOutputStream.writeInt(buffer.length);
				dataOutputStream.write(buffer);
			}	
						
		} catch(Exception e) {
			System.err.println("Critical failure while writing to Android device:\n");
			e.printStackTrace();
		} finally {
			try {
			if(dataOutputStream != null)
				dataOutputStream.close();
			} catch(IOException e) { /* don't care */ }
		}
	}	
}
